# coding: utf-8
"""Simple Django model for Scrum management"""

from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
from django.db.models import signals
from django.dispatch import dispatcher
from django.test.testcases import TestCase

from tagging.fields import TagField

from scrumtool.middleware import get_current_user
import datetime


class Project(models.Model):
    """Model for projects with description and activity."""
    name = models.CharField(_(u'project name'), max_length=50)
    slug = models.SlugField(_(u'project URL'), max_length=50, unique=True)
    active = models.BooleanField(_(u'active'), default=True,
                                 help_text=_("Inactive projects won't be shown on projects dashboard."))
    description = models.TextField(_(u'description'), blank=True,
                                   help_text=_('You can use markdown syntax.'))

    class Meta:
        ordering = ('-active', 'name')
        verbose_name = _('project')
        verbose_name_plural = _('projects')

    def __unicode__(self):
        return self.name

class Impediment(models.Model):
    """Model for per project impediments."""
    project = models.ForeignKey(Project, related_name='impediments', verbose_name=_(u'project'))
    name = models.CharField(_(u'impediment name'), max_length=100)
    is_solved = models.BooleanField(_(u'is solved?'), default=False)
    description = models.TextField(_(u'description'), blank=True,
                                   help_text=_('You can use markdown syntax.'))

    class Meta:
        ordering = ('-is_solved', 'name')
        permissions = (("can_view", _("Can list and view in frontend")),)
        verbose_name = _('impediment')
        verbose_name_plural = _('impediments')

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.project)

class Sprint(models.Model):
    """Sprint metadata."""
    name = models.CharField(_(u'sprint name'), max_length=50)
    slug = models.SlugField(_(u'Sprint URL'), max_length=50)
    project = models.ForeignKey(Project, related_name='sprints', verbose_name=_(u'project'))
    goal = models.CharField(_(u'sprint goal'), max_length=200, blank=True)
    start_date = models.DateField(_(u'start date'))
    end_date = models.DateField(_(u'end date'))
    team = models.ManyToManyField(User, related_name='sprint_team',
                                  verbose_name=_(u'team members'),
                                  help_text=_('Select all team members including product owner and scrum master.'))

    class Meta:
        ordering = ('start_date', 'name')
        unique_together = ('project', 'slug')
        permissions = (("can_view", _("Can list and view in frontend")),)
        verbose_name = _('sprint')
        verbose_name_plural = _('sprints')

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.project)

class ProductBacklogStory(models.Model):
    """Product backlog story."""
    project = models.ForeignKey(Project, related_name='backlog_stories',
                                verbose_name=_(u'in project'))
    priority = models.PositiveIntegerField(_(u'priority'),
                                           help_text=_('Use 200,100, 1... where 200 is the story with greatest priority.'))
    story = models.CharField(_(u'user story'), max_length=100,
                             help_text=_('This should be only a short introduction to story.'))
    story_points = models.PositiveSmallIntegerField(_(u'story estimate'), blank=True, null=True,
                                                    help_text=_('Estimate in story points.'))
    description = models.TextField(_(u'description'), blank=True,
                                   help_text=_('You can use markdown syntax.'))
    sprint = models.ForeignKey(Sprint, related_name='stories',
                               blank=True, null=True,
                               verbose_name=_(u'assigned to sprint'),
                               help_text=_('Leave empty if the backlog story is for future sprints.'))
    #TODO: Some kind of limiting, but is it possible? limit_choices_to = { 'project': self.project})
    tags = TagField(verbose_name=_(u'tags'), max_length=50)

    class Meta:
        ordering = ('-priority',)
        unique_together = ('project', 'priority')
        verbose_name = _('product backlog item')
        verbose_name_plural = _('product backlog items')

    def __unicode__(self):
        if self.sprint:
            return u'%s (%s)' % (self.story, self.sprint)
        return u'%s (%s)' % (self.story, self.project)

class FileAttachments(models.Model):
    """File attachment for product backlog item."""
    backlog_story = models.ForeignKey(ProductBacklogStory, related_name='attachments',
                                      blank=True, null=True)
    description = models.CharField(_(u'description'), max_length=100, core=True)
    file = models.FileField(_(u'attachment'), upload_to='attachments/%Y/%m', core=True)

    class Meta:
        ordering = ('description',)
        verbose_name = _('attachment')
        verbose_name_plural = _('attachments')

class Task(models.Model):
    """Concrete task for the product backlog."""
    backlog_story = models.ForeignKey(ProductBacklogStory, related_name='tasks',
                                      verbose_name=_(u'for story'))
    order = models.PositiveIntegerField(_(u'order'),
                                        help_text=_('Use 1, 2, 3... where 1 is the first task to do.'))
    name = models.CharField(_(u'task name'), max_length=100)
    hours = models.PositiveSmallIntegerField(_(u'hours to complete'),
                                             help_text=_('Hours still needed to complete the task; if 0, task is treated as completed.'))
    responsible = models.ForeignKey(User, related_name='assigned_sprints_tasks',
                                    verbose_name=_(u'rensponsible developer'),
                                    help_text=_('If several developers work on one task, choose the one that is responsible for it.'))
    #TODO: Some kind of limiting, but is it possible? limit_choices_to = { 'sprint_team__in': [self.backlog_story.sprint.pk]})

    class Meta:
        ordering = ('order',)
        unique_together = ('backlog_story', 'order')
        permissions = (("can_view", _("Can list and view in frontend")),)
        verbose_name = _('task')
        verbose_name_plural = _('tasks')

    def __unicode__(self):
        return u'%s %dh (%s)' % (self.name, self.hours, self.backlog_story)

class ProjectLogEntry(models.Model):
    """Project log entry is a modified version of django admin logging model."""
    action_time = models.DateTimeField(auto_now_add=True)
    project = models.ForeignKey(Project, related_name='logs', verbose_name=_(u'project'))
    user = models.ForeignKey(User, related_name='previous_scrum_actions',
                             verbose_name=_(u'changed by'))
    change_message = models.TextField(_('change message'), blank=True)
    old_hours_points = models.PositiveSmallIntegerField(blank=True, null=True)
    content_type = models.ForeignKey(ContentType, blank=True, null=True)
    object_id = models.TextField(_('object id'), blank=True, null=True)

    TASK_MESSAGES_EDIT = (
        ('order', _('order (from %s to %s)')),
        ('name', _('name (from "%s" to "%s")')),
        ('hours', _('hours (from %sh to %sh)')),
        ('responsible', _('responsible person (from %s to %s)')),
    )
    
    BACKLOG_STORY_MESSAGES_EDIT = (
        ('priority', _('priority (from %s to %s)')),
        ('story', _('story (from "%s" to "%s")')),
        ('tags', _('tags (from "%s" to "%s")')),
        ('story_points', _('story points (from %s[SP] to %s[SP])')),
        ('sprint', _('sprint (from "%s" to "%s")')),
        ('description', _('description (from "%s" to "%s")')),
    )

    TASK_MESSAGES_ADD = (
        ('order', _('order (%s)')),
        ('name', _('name ("%s")')),
        ('hours', _('hours (%sh)')),
        ('responsible', _('responsible person (%s)')),
    )
    
    BACKLOG_STORY_MESSAGES_ADD = (
        ('priority', _('priority (%s)')),
        ('story', _('story ("%s")')),
        ('tags', _('tags ("%s")')),
        ('story_points', _('story points (%s[SP])')),
        ('sprint', _('sprint ("%s")')),
        ('description', _('description ("%s")')),
    )

    class Meta:
        ordering = ('-action_time',)
        verbose_name = _('project log entry')
        verbose_name_plural = _('project log entries')

    def get_logged_object(self):
        "Returns the edited object represented by this log entry"
        return self.content_type.get_object_for_this_type(pk=self.object_id)

    def __unicode__(self):
        return u'%s - %s (%s)' % (self.project, self.change_message, self.user)

    @classmethod
    def record_action_presave(cls, sender, instance):
        # If it is a new record, mark it to save in post_save signal.
        if instance.id is None:
            instance._marked_to_save = True
            return
        try:
            current = sender.objects.get(id=instance.id)
        except sender.DoesNotExist, e:
            # Pass, if object have pk set but is not yet in DB (eg. when loading fixtures).
            return
        if sender == Task:
            project=instance.backlog_story.project
            old_hours_points=current.hours
            messages = cls.TASK_MESSAGES_EDIT
        elif sender == ProductBacklogStory:
            project=instance.project
            old_hours_points=current.story_points
            messages = cls.BACKLOG_STORY_MESSAGES_EDIT
        else:
            # Not a supported sender, so do nothing.
            return
        
        change_message = _('Changed ')+\
            ', '.join([message % (getattr(current, field), getattr(instance, field))
                      for field, message in messages
                      if getattr(current, field) != getattr(instance, field)])
        
        ple = cls(
            user=get_current_user(),
            content_type=ContentType.objects.get_for_model(sender),
            object_id=instance.pk,
            project=project,
            old_hours_points=old_hours_points,
            change_message=change_message)
        ple.save()

    @classmethod
    def record_action_postsave(cls, sender, instance):
        # Do nothing if not marked to save (not a new object).
        if not getattr(instance, '_marked_to_save', False):
            return
        del instance._marked_to_save
        if sender == Task:
            project=instance.backlog_story.project
            messages = cls.TASK_MESSAGES_ADD
        elif sender == ProductBacklogStory:
            project=instance.project
            messages = cls.BACKLOG_STORY_MESSAGES_ADD
        else:
            # Not a supported sender, so do nothing.
            return
        
        change_message = _('Added with ')+\
            ', '.join([message % getattr(instance, field)
                      for field, message in messages])
        
        ple = cls(
            user=get_current_user(),
            content_type=ContentType.objects.get_for_model(sender),
            object_id=instance.pk,
            project=project,
            old_hours_points=None,
            change_message=change_message)
        ple.save()

dispatcher.connect(ProjectLogEntry.record_action_presave, sender=ProductBacklogStory, signal=signals.pre_save)
dispatcher.connect(ProjectLogEntry.record_action_presave, sender=Task, signal=signals.pre_save)
dispatcher.connect(ProjectLogEntry.record_action_postsave, sender=ProductBacklogStory, signal=signals.post_save)
dispatcher.connect(ProjectLogEntry.record_action_postsave, sender=Task, signal=signals.post_save)

# Models testing code.

class EntriesLoggingTestCase(TestCase):
    fixtures = ['demo']
    
    def setUp(self):
        self.ct_task = ContentType.objects.get_for_model(Task)
        self.ct_backlog = ContentType.objects.get_for_model(ProductBacklogStory)
        self.ct = ContentType.objects.get_for_model(ProjectLogEntry)
    
    def test_logging_edited_task_entry(self):
        self.assertTrue(self.client.login(username='dev1', password='dev1'))
        self.client.get('/admin/') # Do a trick to initialize threadlocals middleware after logging in.
        old_task = Task.objects.get(pk=3)
        old_task.hours = 3
        old_task.order = 5        
        old_task.name = 'Test2'
        old_task.responsible = User.objects.get(username='dev1')
        old_task.save()
        logged_task = ProjectLogEntry.objects.filter(object_id=3, content_type=self.ct_task).order_by('-action_time')[0]
        self.assertEqual(logged_task.old_hours_points, 7)
        self.assertEqual(logged_task.user.id, User.objects.get(username='dev1').id)
        self.assertEqual(logged_task.change_message, 'Changed order (from 1 to 5), name (from "How to improve survey" to "Test2"), hours (from 7h to 3h), responsible person (from dev2 to dev1)')
        self.assertEqual(logged_task.action_time.day, datetime.datetime.now().day)

    def test_logging_added_task_entry(self):
        self.assertTrue(self.client.login(username='dev1', password='dev1'))
        self.client.get('/admin/') # Do a trick to initialize threadlocals middleware after logging in.
        new_task = Task()
        new_task.backlog_story = ProductBacklogStory.objects.get(pk=1)
        new_task.hours = 6
        new_task.order = 3        
        new_task.name = 'Test3'
        new_task.responsible = User.objects.get(username='dev1')
        new_task.save()
        logged_task = ProjectLogEntry.objects.filter(object_id=new_task.id, content_type=self.ct_task).order_by('-action_time')[0]
        self.assertEqual(logged_task.old_hours_points, None)
        self.assertEqual(logged_task.user.id, User.objects.get(username='dev1').id)
        self.assertEqual(logged_task.change_message, 'Added with order (3), name ("Test3"), hours (6h), responsible person (dev1)')
        self.assertEqual(logged_task.action_time.day, datetime.datetime.now().day)
    
    def test_logging_edited_backlog_entry(self):
        self.assertTrue(self.client.login(username='dev1', password='dev1'))
        self.client.get('/admin/') # Do a trick to initialize threadlocals middleware after logging in.
        old_pbs = ProductBacklogStory.objects.get(pk=1)
        old_pbs.priority = 3
        old_pbs.story = 'New story'
        old_pbs.story_points = 8
        old_pbs.description = 'Test2'
        old_pbs.sprint = None
        old_pbs.tags = 'test'
        old_pbs.save()
        logged_pbs = ProjectLogEntry.objects.filter(object_id=1, content_type=self.ct_backlog).order_by('-action_time')[0]
        self.assertEqual(logged_pbs.old_hours_points, 5)
        self.assertEqual(logged_pbs.user.id, User.objects.get(username='dev1').id)
        self.assertEqual(logged_pbs.change_message, 'Changed priority (from 20 to 3), story (from "Scrum tool round trip" to "New story"), tags (from "demo" to "test"), story points (from 5[SP] to 8[SP]), sprint (from "Sprint 1 (test)" to "None"), description (from "Not much to say." to "Test2")')
        self.assertEqual(logged_pbs.action_time.day, datetime.datetime.now().day)
        old_pbs.story_points = 2
        old_pbs.save()
        logged_pbs = ProjectLogEntry.objects.filter(object_id=1, content_type=self.ct_backlog).order_by('-action_time')[0]
        self.assertEqual(logged_pbs.old_hours_points, 8)
        self.assertEqual(logged_pbs.user.id, User.objects.get(username='dev1').id)
        self.assertEqual(logged_pbs.change_message, 'Changed story points (from 8[SP] to 2[SP])')
        self.assertEqual(logged_pbs.action_time.day, datetime.datetime.now().day)        
        
    def test_logging_added_backlog_entry(self):
        self.assertTrue(self.client.login(username='dev1', password='dev1'))
        self.client.get('/admin/') # Do a trick to initialize threadlocals middleware after logging in.
        new_pbs = ProductBacklogStory()
        new_pbs.project = Project.objects.get(pk=1)
        new_pbs.priority = 9
        new_pbs.story = 'New story'
        new_pbs.story_points = 8
        new_pbs.description = 'Test2'
        new_pbs.sprint = None
        new_pbs.tags = 'test'
        new_pbs.save()
        logged_pbs = ProjectLogEntry.objects.filter(object_id=new_pbs.id, content_type=self.ct_backlog).order_by('-action_time')[0]
        self.assertEqual(logged_pbs.old_hours_points, None)
        self.assertEqual(logged_pbs.user.id, User.objects.get(username='dev1').id)
        self.assertEqual(logged_pbs.change_message, 'Added with priority (9), story ("New story"), tags ("test"), story points (8[SP]), sprint ("None"), description ("Test2")')
        self.assertEqual(logged_pbs.action_time.day, datetime.datetime.now().day)